﻿#if WITH_JIGLIBX

using System;
using System.Collections.Generic;
using System.Threading;

using JigLibX.Physics;
using JigLibX.Collision;
using JigLibX.Geometry;

using Microsoft.Xna.Framework;

using Tomahawk.Runtime.Threading;
using Tomahawk.Resources.Physics;
using Tomahawk.Runtime.Physics.Interfaces;

namespace Tomahawk.Runtime.Physics.JigLibX
{
    internal class PhysicsController : Controller
    {
        #region Properties
        internal enum CoordinateSystem
        {
            WorldCoordinates = 0,
            LocalCoordinates = 1
        }

        internal struct Force
        {
            public CoordinateSystem coordinateSystem;
            public Vector3 force;
            public Vector3 position;
            public PhysicsBody body;
        }

        internal struct Torque
        {
            public CoordinateSystem coordinateSystem;
            public Vector3 torque;
            public PhysicsBody body;
        }

        private Queue<Force> forces = new Queue<Force>();
        internal Queue<Force> Forces { get { return forces; } }

        private Queue<Torque> torques = new Queue<Torque>();
        internal Queue<Torque> Torques { get { return torques; } }
        #endregion

        #region Inherited from Controller
        public override void UpdateController(float elapsedTime)
        {
            // Apply pending forces
            while (forces.Count > 0)
            {
                Force force = forces.Dequeue();
                switch (force.coordinateSystem)
                {
                    case CoordinateSystem.LocalCoordinates: 
                        {
                            force.body.AddBodyForce(force.force, force.position);
                        } break;
                    case CoordinateSystem.WorldCoordinates:
                        {
                            force.body.AddWorldForce(force.force, force.position);
                        } break;
                }
            }

            // Apply pending torques
            while (torques.Count > 0)
            {
                Torque torque = torques.Dequeue();
                switch (torque.coordinateSystem)
                {
                    case CoordinateSystem.LocalCoordinates:
                        {
                            torque.body.AddBodyTorque(torque.torque);
                        } break;
                    case CoordinateSystem.WorldCoordinates:
                        {
                            torque.body.AddWorldTorque(torque.torque);
                        } break;
                }
            }
        }
        #endregion
    }

    public class PhysicsScene : IPhysicsScene
    {
        public enum Status
        {
            Waiting = 0,
            Simulating = 1
        }

        private class PhysicsTaskData
        {
            public float ElapsedTime;
        }

        #region Properties
        private PhysicsSystem physicsSystem = null;
        public PhysicsSystem PhysicsSystem { get { return physicsSystem; } }

        private PhysicsController physicsController = null;
        internal PhysicsController PhysicsController { get { return physicsController; } }

        private Queue<Contact> contacts = new Queue<Contact>();
        internal Queue<Contact> Contacts { get { return contacts; } }

        private Queue<Collision> collisions = new Queue<Collision>();
        internal Queue<Collision> Collisions { get { return collisions; } }
        
        private Status currentStatus = Status.Waiting;
        public Status CurrentStatus { get { return currentStatus; } }

        private ManagedThread workThread = null;
        #endregion

        public PhysicsScene(PhysicsSceneDesc sceneDesc)
        {
            if (PhysicsSystem.CurrentPhysicsSystem != null)
            {
                throw new Exception("Only one physics scene is allowed in JigLibX");
            }

            physicsSystem = new PhysicsSystem();
            physicsController = new PhysicsController();
            physicsSystem.CollisionSystem = new CollisionSystemSAP();
            physicsSystem.AddController(physicsController);

            physicsSystem.EnableFreezing = sceneDesc.enableFreezing;
            physicsSystem.SolverType = (PhysicsSystem.Solver)sceneDesc.solverType;
            physicsSystem.CollisionSystem.UseSweepTests = sceneDesc.enableSweepTest;

            workThread = new ManagedThread();
        }

        public virtual void Dispose()
        {
            workThread.Kill();
        }

        private void Verify(Status validStatus)
        {
            if (currentStatus != validStatus)
            {
                throw new Exception("Invalid operation while simulating physics");
            }
        }

        #region Worker Thread
        private void DoWork(object input)
        {
            physicsSystem.Integrate((input as PhysicsTaskData).ElapsedTime);
            currentStatus = Status.Waiting;
        }
        #endregion

        #region IPhysicsScene

        #region Properties
        public Vector3 GetGravity()
        {
            Verify(Status.Waiting);

            if (physicsSystem != null)
            {
                return physicsSystem.Gravity;
            }

            return Vector3.Zero;
        }

        public void SetGravity(Vector3 gravity)
        {
            Verify(Status.Waiting);

            if (physicsSystem != null)
            {
                physicsSystem.Gravity = gravity;
            }
        }
        #endregion

        #region Physics Simulation
        public bool Simulate(float elapsedTime)
        {
            Verify(Status.Waiting);

            PhysicsTaskData taskData = new PhysicsTaskData();
            taskData.ElapsedTime = elapsedTime;

            currentStatus = Status.Simulating;
            ThreadTask task = new ThreadTask(DoWork, null, 0, taskData);
            workThread.AddTask(task);

            return true;
        }

        public bool WaitForResults(out List<Collision> collisions, out List<Contact> contacts)
        {
            while (currentStatus == Status.Simulating)
            {
                Thread.Sleep(0);
            }

            collisions = new List<Collision>();
            while (this.collisions.Count > 0)
            {
                collisions.Add(this.collisions.Dequeue());
            }

            contacts = new List<Contact>();
            for (int idx = 0; idx < this.contacts.Count; ++idx)
            {
                Contact contact = this.contacts.Dequeue();

                switch (contact.type)
                {
                    case Contact.ContactType.Enter:
                        {
                            contacts.Add((Contact) contact.Clone());
                            contact.type = Contact.ContactType.Stay;
                            this.contacts.Enqueue(contact);
                        } break;
                    case Contact.ContactType.Stay:
                        {
                            contact.type = Contact.ContactType.Exit;
                            this.contacts.Enqueue(contact);
                        } break;
                    case Contact.ContactType.Exit:
                        {
                            contacts.Add(contact);
                        } break;
                }
            }

            return true;
        }
        #endregion

        #region Actors
        public bool CreateActor(string resource, out IPhysicsActor physActor)
        {
            Verify(Status.Waiting);

            physActor = new PhysicsActor(resource, this);
            
            return true;
        }
        public bool DestroyActor(IPhysicsActor physActor)
        {
            (physActor as PhysicsActor).Enabled = false;
            return true;
        }
        #endregion

        #region Queries
        internal class CollisionPredicate : CollisionSkinPredicate1
        {
            private HitFlags flags;

            public CollisionPredicate(HitFlags flags)
            {
                this.flags = flags;
            }

            public override bool ConsiderSkin(CollisionSkin skin0)
            {
                PhysicsBody body = skin0.Owner as PhysicsBody;
                PhysicsActorType type = body.Actor.PhysicsType;

                switch (type)
                {
                    case PhysicsActorType.StaticActor:
                        return (flags & HitFlags.StaticActors) != 0;
                    case PhysicsActorType.InterpActor:
                        return (flags & HitFlags.InterpActors) != 0;
                    case PhysicsActorType.DynamicActor:
                        return (flags & HitFlags.DynamicActors) != 0;
                }

                return false;
            }
        }

        public HitResult SingleLineCheck(Vector3 origin, Vector3 delta, HitFlags flags)
        {
            Verify(Status.Waiting);

            HitResult result = new HitResult();

            float frac = 0.0f;
            CollisionSkin skin = null;
            Segment segment = new Segment(origin, delta);
            CollisionPredicate predicate = new CollisionPredicate(flags);

            bool collided = physicsSystem.CollisionSystem.SegmentIntersect(
                out frac, 
                out skin,
                out result.position,
                out result.normal,
                segment,
                predicate);

            result.actor = null;

            if (skin != null && collided)
            {
                PhysicsBody body = skin.Owner as PhysicsBody;
                result.actor = body.Actor;
                result.element = body.Name;
            }

            return result;
        }
        #endregion
        #endregion
    }
}

#endif